import * as vscode from "vscode"
import * as fs from "fs/promises"
import * as path from "path"
import { Browser, Page, ScreenshotOptions, TimeoutError, launch, connect, KeyInput } from "puppeteer-core"
// @ts-ignore
import PCR from "puppeteer-chromium-resolver"
import pWaitFor from "p-wait-for"
import delay from "delay"
import { fileExistsAtPath } from "../../utils/fs"
import { BrowserActionResult } from "../../shared/ExtensionMessage"
import { discoverChromeHostUrl, tryChromeHostUrl } from "./browserDiscovery"

// Timeout constants
const BROWSER_NAVIGATION_TIMEOUT = 15_000 // 15 seconds

interface PCRStats {
	puppeteer: { launch: typeof launch }
	executablePath: string
}

export class BrowserSession {
	private context: vscode.ExtensionContext
	private browser?: Browser
	private page?: Page
	private currentMousePosition?: string
	private lastConnectionAttempt?: number
	private isUsingRemoteBrowser: boolean = false
	private onStateChange?: (isActive: boolean) => void

	// Track last known viewport to surface in environment details
	private lastViewportWidth?: number
	private lastViewportHeight?: number

	constructor(context: vscode.ExtensionContext, onStateChange?: (isActive: boolean) => void) {
		this.context = context
		this.onStateChange = onStateChange
	}

	private async ensureChromiumExists(): Promise<PCRStats> {
		const globalStoragePath = this.context?.globalStorageUri?.fsPath
		if (!globalStoragePath) {
			throw new Error("Global storage uri is invalid")
		}

		const puppeteerDir = path.join(globalStoragePath, "puppeteer")
		const dirExists = await fileExistsAtPath(puppeteerDir)
		if (!dirExists) {
			await fs.mkdir(puppeteerDir, { recursive: true })
		}

		// if chromium doesn't exist, this will download it to path.join(puppeteerDir, ".chromium-browser-snapshots")
		// if it does exist it will return the path to existing chromium
		const stats: PCRStats = await PCR({
			downloadPath: puppeteerDir,
		})

		return stats
	}

	/**
	 * Gets the viewport size from global state or returns default
	 */
	private getViewport() {
		const size = (this.context.globalState.get("browserViewportSize") as string | undefined) || "900x600"
		const [width, height] = size.split("x").map(Number)
		return { width, height }
	}

	/**
	 * Launches a local browser instance
	 */
	private async launchLocalBrowser(): Promise<void> {
		console.log("Launching local browser")
		const stats = await this.ensureChromiumExists()
		this.browser = await stats.puppeteer.launch({
			args: [
				"--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
			],
			executablePath: stats.executablePath,
			defaultViewport: this.getViewport(),
			// headless: false,
		})
		this.isUsingRemoteBrowser = false
	}

	/**
	 * Connects to a browser using a WebSocket URL
	 */
	private async connectWithChromeHostUrl(chromeHostUrl: string): Promise<boolean> {
		try {
			this.browser = await connect({
				browserURL: chromeHostUrl,
				defaultViewport: this.getViewport(),
			})

			// Cache the successful endpoint
			console.log(`Connected to remote browser at ${chromeHostUrl}`)
			this.context.globalState.update("cachedChromeHostUrl", chromeHostUrl)
			this.lastConnectionAttempt = Date.now()
			this.isUsingRemoteBrowser = true

			return true
		} catch (error) {
			console.log(`Failed to connect using WebSocket endpoint: ${error}`)
			return false
		}
	}

	/**
	 * Attempts to connect to a remote browser using various methods
	 * Returns true if connection was successful, false otherwise
	 */
	private async connectToRemoteBrowser(): Promise<boolean> {
		let remoteBrowserHost = this.context.globalState.get("remoteBrowserHost") as string | undefined
		let reconnectionAttempted = false

		// Try to connect with cached endpoint first if it exists and is recent (less than 1 hour old)
		const cachedChromeHostUrl = this.context.globalState.get("cachedChromeHostUrl") as string | undefined
		if (cachedChromeHostUrl && this.lastConnectionAttempt && Date.now() - this.lastConnectionAttempt < 3_600_000) {
			console.log(`Attempting to connect using cached Chrome Host Url: ${cachedChromeHostUrl}`)
			if (await this.connectWithChromeHostUrl(cachedChromeHostUrl)) {
				return true
			}

			console.log(`Failed to connect using cached Chrome Host Url: ${cachedChromeHostUrl}`)
			// Clear the cached endpoint since it's no longer valid
			this.context.globalState.update("cachedChromeHostUrl", undefined)

			// User wants to give up after one reconnection attempt
			if (remoteBrowserHost) {
				reconnectionAttempted = true
			}
		}

		// If user provided a remote browser host, try to connect to it
		else if (remoteBrowserHost && !reconnectionAttempted) {
			console.log(`Attempting to connect to remote browser at ${remoteBrowserHost}`)
			try {
				const hostIsValid = await tryChromeHostUrl(remoteBrowserHost)

				if (!hostIsValid) {
					throw new Error("Could not find chromeHostUrl in the response")
				}

				console.log(`Found WebSocket endpoint: ${remoteBrowserHost}`)

				if (await this.connectWithChromeHostUrl(remoteBrowserHost)) {
					return true
				}
			} catch (error) {
				console.error(`Failed to connect to remote browser: ${error}`)
				// Fall back to auto-discovery if remote connection fails
			}
		}

		try {
			console.log("Attempting browser auto-discovery...")
			const chromeHostUrl = await discoverChromeHostUrl()

			if (chromeHostUrl && (await this.connectWithChromeHostUrl(chromeHostUrl))) {
				return true
			}
		} catch (error) {
			console.error(`Auto-discovery failed: ${error}`)
			// Fall back to local browser if auto-discovery fails
		}

		return false
	}

	async launchBrowser(): Promise<void> {
		console.log("launch browser called")

		// Check if remote browser connection is enabled
		const remoteBrowserEnabled = this.context.globalState.get("remoteBrowserEnabled") as boolean | undefined

		if (!remoteBrowserEnabled) {
			console.log("Launching local browser")
			if (this.browser) {
				// throw new Error("Browser already launched")
				await this.closeBrowser() // this may happen when the model launches a browser again after having used it already before
			} else {
				// If browser wasn't open, just reset the state
				this.resetBrowserState()
			}
			await this.launchLocalBrowser()
		} else {
			console.log("Connecting to remote browser")
			// Remote browser connection is enabled
			const remoteConnected = await this.connectToRemoteBrowser()

			// If all remote connection attempts fail, fall back to local browser
			if (!remoteConnected) {
				console.log("Falling back to local browser")
				await this.launchLocalBrowser()
			}
		}

		// Notify that browser session is now active
		if (this.browser && this.onStateChange) {
			this.onStateChange(true)
		}
	}

	/**
	 * Closes the browser and resets browser state
	 */
	async closeBrowser(): Promise<BrowserActionResult> {
		const wasActive = !!(this.browser || this.page)

		if (wasActive) {
			if (this.isUsingRemoteBrowser && this.browser) {
				await this.browser.disconnect().catch(() => {})
			} else {
				await this.browser?.close().catch(() => {})
			}
			this.resetBrowserState()

			// Notify that browser session is now inactive
			if (this.onStateChange) {
				this.onStateChange(false)
			}
		}
		return {}
	}

	/**
	 * Resets all browser state variables
	 */
	private resetBrowserState(): void {
		this.browser = undefined
		this.page = undefined
		this.currentMousePosition = undefined
		this.isUsingRemoteBrowser = false
		this.lastViewportWidth = undefined
		this.lastViewportHeight = undefined
	}

	async doAction(action: (page: Page) => Promise<void>): Promise<BrowserActionResult> {
		if (!this.page) {
			throw new Error(
				"Cannot perform browser action: no active browser session. The browser must be launched first using the 'launch' action before other browser actions can be performed.",
			)
		}

		const logs: string[] = []
		let lastLogTs = Date.now()

		const consoleListener = (msg: any) => {
			if (msg.type() === "log") {
				logs.push(msg.text())
			} else {
				logs.push(`[${msg.type()}] ${msg.text()}`)
			}
			lastLogTs = Date.now()
		}

		const errorListener = (err: Error) => {
			logs.push(`[Page Error] ${err.toString()}`)
			lastLogTs = Date.now()
		}

		// Add the listeners
		this.page.on("console", consoleListener)
		this.page.on("pageerror", errorListener)

		try {
			await action(this.page)
		} catch (err) {
			if (!(err instanceof TimeoutError)) {
				logs.push(`[Error] ${err.toString()}`)
			}
		}

		// Wait for console inactivity, with a timeout
		await pWaitFor(() => Date.now() - lastLogTs >= 500, {
			timeout: 3_000,
			interval: 100,
		}).catch(() => {})

		// Draw cursor indicator if we have a cursor position
		if (this.currentMousePosition) {
			await this.drawCursorIndicator(this.page, this.currentMousePosition)
		}

		let options: ScreenshotOptions = {
			encoding: "base64",

			// clip: {
			// 	x: 0,
			// 	y: 0,
			// 	width: 900,
			// 	height: 600,
			// },
		}

		let screenshotBase64 = await this.page.screenshot({
			...options,
			type: "webp",
			quality: ((await this.context.globalState.get("screenshotQuality")) as number | undefined) ?? 75,
		})
		let screenshot = `data:image/webp;base64,${screenshotBase64}`

		if (!screenshotBase64) {
			console.log("webp screenshot failed, trying png")
			screenshotBase64 = await this.page.screenshot({
				...options,
				type: "png",
			})
			screenshot = `data:image/png;base64,${screenshotBase64}`
		}

		if (!screenshotBase64) {
			throw new Error("Failed to take screenshot.")
		}

		// Remove cursor indicator after taking screenshot
		if (this.currentMousePosition) {
			await this.removeCursorIndicator(this.page)
		}

		// this.page.removeAllListeners() <- causes the page to crash!
		this.page.off("console", consoleListener)
		this.page.off("pageerror", errorListener)

		// Get actual viewport dimensions
		const viewport = this.page.viewport()

		// Persist last known viewport dimensions
		this.lastViewportWidth = viewport?.width
		this.lastViewportHeight = viewport?.height

		return {
			screenshot,
			logs: logs.join("\n"),
			currentUrl: this.page.url(),
			currentMousePosition: this.currentMousePosition,
			viewportWidth: viewport?.width,
			viewportHeight: viewport?.height,
		}
	}

	/**
	 * Extract the root domain from a URL
	 * e.g., http://localhost:3000/path -> localhost:3000
	 * e.g., https://example.com/path -> example.com
	 */
	private getRootDomain(url: string): string {
		try {
			const urlObj = new URL(url)
			// Remove www. prefix if present
			return urlObj.host.replace(/^www\./, "")
		} catch (error) {
			// If URL parsing fails, return the original URL
			return url
		}
	}

	/**
	 * Navigate to a URL with standard loading options
	 */
	private async navigatePageToUrl(page: Page, url: string): Promise<void> {
		await page.goto(url, { timeout: BROWSER_NAVIGATION_TIMEOUT, waitUntil: ["domcontentloaded", "networkidle2"] })
		await this.waitTillHTMLStable(page)
	}

	/**
	 * Creates a new tab and navigates to the specified URL
	 */
	private async createNewTab(url: string): Promise<BrowserActionResult> {
		if (!this.browser) {
			throw new Error("Browser is not launched")
		}

		// Create a new page
		const newPage = await this.browser.newPage()

		// Set the new page as the active page
		this.page = newPage

		// Navigate to the URL
		const result = await this.doAction(async (page) => {
			await this.navigatePageToUrl(page, url)
		})

		return result
	}

	async navigateToUrl(url: string): Promise<BrowserActionResult> {
		if (!this.browser) {
			throw new Error("Browser is not launched")
		}
		// Remove trailing slash for comparison
		const normalizedNewUrl = url.replace(/\/$/, "")

		// Extract the root domain from the URL
		const rootDomain = this.getRootDomain(normalizedNewUrl)

		// Get all current pages
		const pages = await this.browser.pages()

		// Try to find a page with the same root domain
		let existingPage: Page | undefined

		for (const page of pages) {
			try {
				const pageUrl = page.url()
				if (pageUrl && this.getRootDomain(pageUrl) === rootDomain) {
					existingPage = page
					break
				}
			} catch (error) {
				// Skip pages that might have been closed or have errors
				console.log(`Error checking page URL: ${error}`)
				continue
			}
		}

		if (existingPage) {
			// Tab with the same root domain exists, switch to it
			console.log(`Tab with domain ${rootDomain} already exists, switching to it`)

			// Update the active page
			this.page = existingPage
			existingPage.bringToFront()

			// Navigate to the new URL if it's different]
			const currentUrl = existingPage.url().replace(/\/$/, "") // Remove trailing / if present
			if (this.getRootDomain(currentUrl) === rootDomain && currentUrl !== normalizedNewUrl) {
				console.log(`Navigating to new URL: ${normalizedNewUrl}`)
				console.log(`Current URL: ${currentUrl}`)
				console.log(`Root domain: ${this.getRootDomain(currentUrl)}`)
				console.log(`New URL: ${normalizedNewUrl}`)
				// Navigate to the new URL
				return this.doAction(async (page) => {
					await this.navigatePageToUrl(page, normalizedNewUrl)
				})
			} else {
				console.log(`Tab with domain ${rootDomain} already exists, and URL is the same: ${normalizedNewUrl}`)
				// URL is the same, just reload the page to ensure it's up to date
				console.log(`Reloading page: ${normalizedNewUrl}`)
				console.log(`Current URL: ${currentUrl}`)
				console.log(`Root domain: ${this.getRootDomain(currentUrl)}`)
				console.log(`New URL: ${normalizedNewUrl}`)
				return this.doAction(async (page) => {
					await page.reload({
						timeout: BROWSER_NAVIGATION_TIMEOUT,
						waitUntil: ["domcontentloaded", "networkidle2"],
					})
					await this.waitTillHTMLStable(page)
				})
			}
		} else {
			// No tab with this root domain exists, create a new one
			console.log(`No tab with domain ${rootDomain} exists, creating a new one`)
			return this.createNewTab(normalizedNewUrl)
		}
	}

	// page.goto { waitUntil: "networkidle0" } may not ever resolve, and not waiting could return page content too early before js has loaded
	// https://stackoverflow.com/questions/52497252/puppeteer-wait-until-page-is-completely-loaded/61304202#61304202
	private async waitTillHTMLStable(page: Page, timeout = 5_000) {
		const checkDurationMsecs = 500 // 1000
		const maxChecks = timeout / checkDurationMsecs
		let lastHTMLSize = 0
		let checkCounts = 1
		let countStableSizeIterations = 0
		const minStableSizeIterations = 3

		while (checkCounts++ <= maxChecks) {
			let html = await page.content()
			let currentHTMLSize = html.length

			// let bodyHTMLSize = await page.evaluate(() => document.body.innerHTML.length)
			console.log("last: ", lastHTMLSize, " <> curr: ", currentHTMLSize)

			if (lastHTMLSize !== 0 && currentHTMLSize === lastHTMLSize) {
				countStableSizeIterations++
			} else {
				countStableSizeIterations = 0 //reset the counter
			}

			if (countStableSizeIterations >= minStableSizeIterations) {
				console.log("Page rendered fully...")
				break
			}

			lastHTMLSize = currentHTMLSize
			await delay(checkDurationMsecs)
		}
	}

	/**
	 * Force links and window.open to navigate in the same tab.
	 * This makes clicks on anchors with target="_blank" stay in the current page
	 * and also intercepts window.open so SPA/open-in-new-tab patterns don't spawn popups.
	 */
	private async forceLinksToSameTab(page: Page): Promise<void> {
		try {
			await page.evaluate(() => {
				try {
					// Ensure we only install once per document
					if ((window as any).__ROO_FORCE_SAME_TAB__) return
					;(window as any).__ROO_FORCE_SAME_TAB__ = true

					// Override window.open to navigate current tab instead of creating a new one
					const originalOpen = window.open
					window.open = function (url: string | URL, target?: string, features?: string) {
						try {
							const href = typeof url === "string" ? url : String(url)
							location.href = href
						} catch {
							// fall back to original if something unexpected occurs
							try {
								return originalOpen.apply(window, [url as any, "_self", features]) as any
							} catch {}
						}
						return null as any
					} as any

					// Rewrite anchors that explicitly open new tabs
					document.querySelectorAll('a[target="_blank"]').forEach((a) => {
						a.setAttribute("target", "_self")
					})

					// Defensive capture: if an element still tries to open in a new tab, force same-tab
					document.addEventListener(
						"click",
						(ev) => {
							const el = (ev.target as HTMLElement | null)?.closest?.(
								'a[target="_blank"]',
							) as HTMLAnchorElement | null
							if (el && el.href) {
								ev.preventDefault()
								try {
									location.href = el.href
								} catch {}
							}
						},
						{ capture: true, passive: false },
					)
				} catch {
					// no-op; forcing same-tab is best-effort
				}
			})
		} catch {
			// If evaluate fails (e.g., cross-origin/state), continue without breaking the action
		}
	}

	/**
	 * Handles mouse interaction with network activity monitoring
	 */
	private async handleMouseInteraction(
		page: Page,
		coordinate: string,
		action: (x: number, y: number) => Promise<void>,
	): Promise<void> {
		const [x, y] = coordinate.split(",").map(Number)

		// Force any new-tab behavior (target="_blank", window.open) to stay in the same tab
		await this.forceLinksToSameTab(page)

		// Set up network request monitoring
		let hasNetworkActivity = false
		const requestListener = () => {
			hasNetworkActivity = true
		}
		page.on("request", requestListener)

		// Perform the mouse action
		await action(x, y)
		this.currentMousePosition = coordinate

		// Small delay to check if action triggered any network activity
		await delay(100)

		if (hasNetworkActivity) {
			// If we detected network activity, wait for navigation/loading
			await page
				.waitForNavigation({
					waitUntil: ["domcontentloaded", "networkidle2"],
					timeout: BROWSER_NAVIGATION_TIMEOUT,
				})
				.catch(() => {})
			await this.waitTillHTMLStable(page)
		}

		// Clean up listener
		page.off("request", requestListener)
	}

	async click(coordinate: string): Promise<BrowserActionResult> {
		return this.doAction(async (page) => {
			await this.handleMouseInteraction(page, coordinate, async (x, y) => {
				await page.mouse.click(x, y)
			})
		})
	}

	async type(text: string): Promise<BrowserActionResult> {
		return this.doAction(async (page) => {
			await page.keyboard.type(text)
		})
	}

	async press(key: string): Promise<BrowserActionResult> {
		return this.doAction(async (page) => {
			// Parse key combinations (e.g., "Cmd+K", "Shift+Enter")
			const parts = key.split("+").map((k) => k.trim())
			const modifiers: string[] = []
			let mainKey = parts[parts.length - 1]

			// Identify modifiers
			for (let i = 0; i < parts.length - 1; i++) {
				const part = parts[i].toLowerCase()
				if (part === "cmd" || part === "command" || part === "meta") {
					modifiers.push("Meta")
				} else if (part === "ctrl" || part === "control") {
					modifiers.push("Control")
				} else if (part === "shift") {
					modifiers.push("Shift")
				} else if (part === "alt" || part === "option") {
					modifiers.push("Alt")
				}
			}

			// Map common key aliases to Puppeteer KeyInput values
			const mapping: Record<string, KeyInput | string> = {
				esc: "Escape",
				return: "Enter",
				escape: "Escape",
				enter: "Enter",
				tab: "Tab",
				space: "Space",
				arrowup: "ArrowUp",
				arrowdown: "ArrowDown",
				arrowleft: "ArrowLeft",
				arrowright: "ArrowRight",
			}
			mainKey = (mapping[mainKey.toLowerCase()] ?? mainKey) as string

			// Avoid new-tab behavior from Enter on links/buttons
			await this.forceLinksToSameTab(page)

			// Track inflight requests so we can detect brief network bursts
			let inflight = 0
			const onRequest = () => {
				inflight++
			}
			const onRequestDone = () => {
				inflight = Math.max(0, inflight - 1)
			}
			page.on("request", onRequest)
			page.on("requestfinished", onRequestDone)
			page.on("requestfailed", onRequestDone)

			// Start a short navigation wait in parallel; if no nav, it times out harmlessly
			const HARD_CAP_MS = 3000
			const navPromise = page
				.waitForNavigation({
					// domcontentloaded is enough to confirm a submit navigated
					waitUntil: ["domcontentloaded"],
					timeout: HARD_CAP_MS,
				})
				.catch(() => undefined)

			// Press key combination
			if (modifiers.length > 0) {
				// Hold down modifiers
				for (const modifier of modifiers) {
					await page.keyboard.down(modifier as KeyInput)
				}

				// Press main key
				await page.keyboard.press(mainKey as KeyInput)

				// Release modifiers
				for (const modifier of modifiers) {
					await page.keyboard.up(modifier as KeyInput)
				}
			} else {
				// Single key press
				await page.keyboard.press(mainKey as KeyInput)
			}

			// Give time for any requests to kick off
			await delay(120)

			// Hard-cap the wait to avoid UI hangs
			await Promise.race([
				navPromise,
				pWaitFor(() => inflight === 0, { timeout: HARD_CAP_MS, interval: 100 }).catch(() => {}),
				delay(HARD_CAP_MS),
			])

			// Stabilize DOM briefly before capturing screenshot (shorter cap)
			await this.waitTillHTMLStable(page, 2_000)

			// Cleanup
			page.off("request", onRequest)
			page.off("requestfinished", onRequestDone)
			page.off("requestfailed", onRequestDone)
		})
	}

	/**
	 * Scrolls the page by the specified amount
	 */
	private async scrollPage(page: Page, direction: "up" | "down"): Promise<void> {
		const { height } = this.getViewport()
		const scrollAmount = direction === "down" ? height : -height

		await page.evaluate((scrollHeight) => {
			window.scrollBy({
				top: scrollHeight,
				behavior: "auto",
			})
		}, scrollAmount)

		await delay(300)
	}

	async scrollDown(): Promise<BrowserActionResult> {
		return this.doAction(async (page) => {
			await this.scrollPage(page, "down")
		})
	}

	async scrollUp(): Promise<BrowserActionResult> {
		return this.doAction(async (page) => {
			await this.scrollPage(page, "up")
		})
	}

	async hover(coordinate: string): Promise<BrowserActionResult> {
		return this.doAction(async (page) => {
			await this.handleMouseInteraction(page, coordinate, async (x, y) => {
				await page.mouse.move(x, y)
				// Small delay to allow any hover effects to appear
				await delay(300)
			})
		})
	}

	async resize(size: string): Promise<BrowserActionResult> {
		return this.doAction(async (page) => {
			const [width, height] = size.split(",").map(Number)
			const session = await page.createCDPSession()
			await page.setViewport({ width, height })
			const { windowId } = await session.send("Browser.getWindowForTarget")
			await session.send("Browser.setWindowBounds", {
				bounds: { width, height },
				windowId,
			})
		})
	}

	/**
	 * Draws a cursor indicator on the page at the specified position
	 */
	private async drawCursorIndicator(page: Page, coordinate: string): Promise<void> {
		const [x, y] = coordinate.split(",").map(Number)

		try {
			await page.evaluate(
				(cursorX: number, cursorY: number) => {
					// Create a cursor indicator element
					const cursor = document.createElement("div")
					cursor.id = "__roo_cursor_indicator__"
					cursor.style.cssText = `
						position: fixed;
						left: ${cursorX}px;
						top: ${cursorY}px;
						width: 35px;
						height: 35px;
						pointer-events: none;
						z-index: 2147483647;
					`

					// Create SVG cursor pointer
					const svg = `
						<svg width="35" height="35" viewBox="0 0 35 35" fill="none" xmlns="http://www.w3.org/2000/svg">
							<path d="M5 3L5 17L9 13L12 19L14 18L11 12L17 12L5 3Z"
								  fill="white"
								  stroke="black"
								  stroke-width="1.5"/>
							<path d="M5 3L5 17L9 13L12 19L14 18L11 12L17 12L5 3Z"
								  fill="black"
								  stroke="white"
								  stroke-width=".5"/>
						</svg>
					`
					cursor.innerHTML = svg

					document.body.appendChild(cursor)
				},
				x,
				y,
			)
		} catch (error) {
			console.error("Failed to draw cursor indicator:", error)
		}
	}

	/**
	 * Removes the cursor indicator from the page
	 */
	private async removeCursorIndicator(page: Page): Promise<void> {
		try {
			await page.evaluate(() => {
				const cursor = document.getElementById("__roo_cursor_indicator__")
				if (cursor) {
					cursor.remove()
				}
			})
		} catch (error) {
			console.error("Failed to remove cursor indicator:", error)
		}
	}

	/**
	 * Returns whether a browser session is currently active
	 */
	isSessionActive(): boolean {
		return !!(this.browser && this.page)
	}

	/**
	 * Returns the last known viewport size (if any)
	 *
	 * Prefer the live page viewport when available so we stay accurate after:
	 * - browser_action resize
	 * - manual window resizes (especially with remote browsers)
	 *
	 * Falls back to the configured default viewport when no prior information exists.
	 */
	getViewportSize(): { width?: number; height?: number } {
		// If we have an active page, ask Puppeteer for the current viewport.
		// This keeps us in sync with any resizes that happen outside of our own
		// browser_action lifecycle (e.g. user dragging the window).
		if (this.page) {
			const vp = this.page.viewport()
			if (vp?.width) this.lastViewportWidth = vp.width
			if (vp?.height) this.lastViewportHeight = vp.height
		}

		// If we've ever observed a viewport, use that.
		if (this.lastViewportWidth && this.lastViewportHeight) {
			return {
				width: this.lastViewportWidth,
				height: this.lastViewportHeight,
			}
		}

		// Otherwise fall back to the configured default so the tool can still
		// operate before the first screenshot-based action has run.
		const { width, height } = this.getViewport()
		return { width, height }
	}
}
